/**
 * Copyright 2007 Jason Horman, Pete Aykroyd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.functor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * A functor that takes one argument and returns a <code>boolean</code> value.
 */
public abstract class UnaryPredicate<ParamType> extends BooleanFunctor<UnaryPredicate<ParamType>> {
	private static final long serialVersionUID = 0;

	public static final UnaryPredicate TRUE = new True();
	public static final UnaryPredicate FALSE = new False();
	public static final UnaryPredicate isNull = isSame(null);
	public static final UnaryPredicate isNotNull = isSame(null).not();

	/** Evaluate this predicate. */
	public abstract boolean test(ParamType obj);

	/** Tests if objects passed to test are .equals to equalsThis. */
	public static UnaryPredicate isEqual(final Object equalsThis) {
		return BinaryPredicate.isEqual(equalsThis);
	}

	/** Tests if objects passed to test are NOT .equals to equalsThis. */
	public static UnaryPredicate isNotEqual(final Object equalsThis) {
		return BinaryPredicate.isNotEqual(equalsThis);
	}

	/** Tests if objects passed to test are == to equalsThis. */
	public static UnaryPredicate isSame(final Object sameAsThis) {
		return BinaryPredicate.isSame(sameAsThis);
	}

	/** Tests if objects passed to test are != to equalsThis. */
	public static UnaryPredicate isNotSame(final Object sameAsThis) {
		return BinaryPredicate.isNotSame(sameAsThis);
	}

	/** Tests if objects passed are instancesOf ofThis class type. */
	public static UnaryPredicate isInstance(final Class ofThis) {
		return new IsInstanceOf(ofThis);
	}

	/** Tests if objects passed are assignable from the class type passed in. */
	public static UnaryPredicate isAssignableFrom(final Class fromThis) {
		return new IsAssignableFrom(fromThis);
	}

	/** Returns a predicate which is the "not" of the predicate passed in. */
	public static UnaryPredicate not(final UnaryPredicate predicate) {
		return new Not(predicate);
	}

	/** Returns a predicate which is the "not" of this predicate. */
	public UnaryPredicate not() {
		return not(this);
	}

	/** And this predicate with another. */
	public UnaryPredicate<ParamType> and(final UnaryPredicate<ParamType> predicate) {
		return new And<ParamType>(this, predicate);
	}

	/** Returns "and(UnaryPredicate.not(predicate))". */
	public UnaryPredicate andNot(final UnaryPredicate<ParamType> predicate) {
		return and(UnaryPredicate.not(predicate));
	}

	/** Or this predicate with another. */
	public UnaryPredicate<ParamType> or(final UnaryPredicate<ParamType> predicate) {
		return new Or<ParamType>(this, predicate);
	}

	/** If this predicate evaluates to true the procedure will be run. */
	public UnaryProcedure ifTrue(final UnaryProcedure<ParamType> proc) {
		return new IfTrueProcedure(proc);
	}

	/** If this predicate evaluates to false the procedure will be run. */
	public UnaryProcedure ifFalse(final UnaryProcedure<ParamType> proc) {
		return new IfFalseProcedure(proc);
	}

	/**
	 * If this predicate evaluates to true onTrue will run, if not onFalse will
	 * run.
	 */
	public UnaryProcedure ifElse(final UnaryProcedure<ParamType> onTrue, final UnaryProcedure<ParamType> onFalse) {
		return new IfElseProcedure(onTrue, onFalse);
	}

	/**
	 * Returns a predicate that will run the onTrue predicate if true, or the
	 * onFalse predicate if false.
	 */
	public UnaryPredicate ifElse(final UnaryPredicate<ParamType> onTrue, final UnaryPredicate<ParamType> onFalse) {
		return new IfElsePredicate(this, onTrue, onFalse);
	}

	public UnaryFunction ifTrue(final UnaryFunction<ParamType, ?> func) {
		return new IfTrueFunction(func);
	}

	public UnaryFunction ifFalse(final UnaryFunction<ParamType, ?> func) {
		return new IfFalseFunction(func);
	}

	public UnaryFunction ifElse(final UnaryFunction<ParamType, ?> onTrue, final UnaryFunction<ParamType, ?> onFalse) {
		return new IfElseFunction(onTrue, onFalse);
	}

	/** Turns a UnaryPredicate into a Predicate and binds its param to the value. */
	public Predicate bind(final ParamType value) {
		return new Binding<ParamType>(this, value);
	}

	/** Is this expression always true? Only "true" for instances of True. */
	public boolean isAlwaysTrue() {
		return false;
	}

	/** Is this expression always false? Only "true" for instances of False. */
	public boolean isAlwaysFalse() {
		return false;
	}

	/** Returns a predicate that always returns true. */
	public static <ParamType> UnaryPredicate<ParamType> isTrue() {
		return TRUE;
	}

	/** Returns a predicate that always returns false. */
	public static <ParamType> UnaryPredicate<ParamType> isFalse() {
		return FALSE;
	}

	public String getDisplay() {
		return toString().replace("~(", "NOT(");
	}

	/**
	 * Create a unary predicate for the given expression.
	 *
	 * @param forExpression The expression.
	 * @param paramClass		The type of the functions parameter.
	 * @return A UnaryPredicate that evaluates the expression.
	 */
	public static <P> UnaryPredicate<P> create(String forExpression, Class paramClass) {
		return new DynamicUnaryPredicate<P>(forExpression, paramClass);
	}

	/**
	 * And's a number of {@link UnaryPredicate unary predicates} together. Short
	 * circuits, meaning the first false stops the test.
	 */
	public static class And<ParamType> extends UnaryJunction<ParamType> implements org.functor.And<UnaryPredicate<ParamType>> {
		private static final long serialVersionUID = 0;

		public List<UnaryPredicate<ParamType>> predicates;

		public And() {
			this.predicates = new ArrayList<UnaryPredicate<ParamType>>();
		}

		public And(UnaryPredicate<ParamType> ... predicates) {
			this(Arrays.asList(predicates));
		}

		public And(List<UnaryPredicate<ParamType>> predicates) {
			this.predicates = predicates;
		}

		public void add(UnaryPredicate<ParamType> predicate) {
			predicates.add(predicate);
		}

		public boolean isEmpty() {
			return predicates.isEmpty();
		}

		public boolean test(ParamType o) {
			for (UnaryPredicate predicate : predicates) {
				if (!predicate.test(o))
					return false;
			}

			return true;
		}

		public List<UnaryPredicate<ParamType>> getPredicates() {
			return predicates;
		}

		/**
		 * Returns an optimized predicate. Example optimizations. Remove any
		 * alwaysTrue predicates. If there are any alwaysFalse predicates return a
		 * predicate which always returns false. If there is only one predicate in the
		 * AND, return that predicate.
		 */
		public UnaryPredicate<ParamType> optimize() {
			And<ParamType> optimized = new And<ParamType>();
			for (UnaryPredicate predicate : getPredicates()) {
				// optimize the predicate from the bottom up
				predicate = (UnaryPredicate) predicate.optimize();
				if (predicate.isAlwaysFalse()) {
					return UnaryPredicate.isFalse();
				} else if (!predicate.isAlwaysTrue()) {
					optimized.add(predicate);
				}
			}

			// this means there were no predicates that weren't always true.
			if (optimized.isEmpty())
				return isTrue();
			// if there is only 1 predicate left there is no need to have an AND
			if (optimized.getPredicates().size() == 1)
				return optimized.getPredicates().get(0);

			return optimized;
		}

		public String toString() {
			StringBuffer str = new StringBuffer();
			for (Iterator<UnaryPredicate<ParamType>> iterator = predicates.iterator(); iterator.hasNext();) {
				UnaryPredicate<ParamType> predicate = iterator.next();
				str.append(predicate);
				if (iterator.hasNext())
					str.append(" AND ");
			}

			return str.toString();
		}
	}

	/** Or's a number of {@link UnaryPredicate unary predicates} together. */
	public static class Or<ParamType> extends UnaryJunction<ParamType> implements org.functor.Or<UnaryPredicate<ParamType>> {
		private static final long serialVersionUID = 0;

		public List<UnaryPredicate<ParamType>> predicates;

		public Or() {
			this.predicates = new ArrayList<UnaryPredicate<ParamType>>();
		}

		public Or(UnaryPredicate<ParamType> ... predicates) {
			this(Arrays.asList(predicates));
		}

		public Or(List<UnaryPredicate<ParamType>> predicates) {
			this.predicates = predicates;
		}

		public void add(UnaryPredicate<ParamType> predicate) {
			predicates.add(predicate);
		}

		public boolean isEmpty() {
			return predicates.isEmpty();
		}

		public boolean test(ParamType o) {
			for (UnaryPredicate<ParamType> predicate : predicates) {
				if (predicate.test(o))
					return true;
			}

			return false;
		}

		public List<UnaryPredicate<ParamType>> getPredicates() {
			return predicates;
		}

		/**
		 * Returns an optimized predicate. Example optimizations. If there are any
		 * alwaysTrue predicates, returns a predicate which always returns true. If
		 * there is only one predicate in the OR, return that predicate.
		 */
		public UnaryPredicate<ParamType> optimize() {
			Or<ParamType> optimized = new Or<ParamType>();
			for (UnaryPredicate predicate : getPredicates()) {
				// optimize the predicate from the bottom up
				predicate = (UnaryPredicate) predicate.optimize();
				if (predicate.isAlwaysTrue())
					return isTrue();
				if (!predicate.isAlwaysFalse())
					optimized.add(predicate);
			}

			// this means there were no predicates that weren't always false.
			if (optimized.isEmpty())
				return isTrue();
			// if there is only 1 predicate left there is no need to have an OR
			if (optimized.getPredicates().size() == 1)
				return optimized.getPredicates().get(0);

			return optimized;
		}

		public String toString() {
			StringBuffer str = new StringBuffer();
			for (Iterator<UnaryPredicate<ParamType>> iterator = predicates.iterator(); iterator.hasNext();) {
				UnaryPredicate<ParamType> predicate = iterator.next();
				if (predicate instanceof And || predicate instanceof Or) {
					str.append('(');
					str.append(predicate);
					str.append(')');
				} else {
					str.append(predicate);
				}

				if (iterator.hasNext())
					str.append(" OR ");
			}

			return str.toString();
		}
	}

	public static class IsAssignableFrom<ParamType> extends UnaryPredicate<ParamType> {
		private final Class fromThis;

		public IsAssignableFrom(Class fromThis) {
			this.fromThis = fromThis;
		}

		public boolean test(Object obj) {
			return fromThis.isAssignableFrom((Class) obj);
		}

		public String toString() {
			return "isAssignableFrom(" + fromThis + ")";
		}
	}

	public static class Not<ParamType> extends UnaryPredicate<ParamType> implements org.functor.Not<UnaryPredicate<ParamType>> {
		private static final long serialVersionUID = 0;

		private final UnaryPredicate predicate;

		public Not(UnaryPredicate predicate) {
			this.predicate = predicate;
		}

		public boolean test(Object obj) {
			return !predicate.test(obj);
		}

    public UnaryPredicate<ParamType> getPredicate() {
			return predicate;
		}


		public Functor optimize() {
			if (predicate.isAlwaysTrue())
				return isFalse();
			else if (predicate.isAlwaysFalse())
				return isTrue();
			return this;
		}

		public String toString() {
			return "~(" + predicate + ")";
		}

		public static final UnaryFunction<UnaryPredicate.Not, UnaryPredicate> GetPredicate =
				new UnaryFunction<UnaryPredicate.Not, UnaryPredicate>() {
					public UnaryPredicate evaluate(Not not) {
						return not.getPredicate();
					}
				};
	}

	public class IfTrueProcedure extends UnaryProcedure<ParamType> {
		private static final long serialVersionUID = 0;
		private final UnaryProcedure<ParamType> proc;

		public IfTrueProcedure(UnaryProcedure<ParamType> proc) {
			this.proc = proc;
		}

		public void run(ParamType obj) {
			if (test(obj))
				proc.run(obj);
		}

  }

	public class IfFalseProcedure extends UnaryProcedure<ParamType> {
		private static final long serialVersionUID = 0;
		private final UnaryProcedure<ParamType> proc;

		public IfFalseProcedure(UnaryProcedure<ParamType> proc) {
			this.proc = proc;
		}

		public void run(ParamType obj) {
			if (!test(obj))
				proc.run(obj);
		}

  }

	public class IfElseProcedure extends UnaryProcedure<ParamType> {
		private static final long serialVersionUID = 0;
		private final UnaryProcedure<ParamType> onTrue;
		private final UnaryProcedure<ParamType> onFalse;

		public IfElseProcedure(UnaryProcedure<ParamType> onTrue, UnaryProcedure<ParamType> onFalse) {
			this.onTrue = onTrue;
			this.onFalse = onFalse;
		}

		public void run(ParamType obj) {
			if (test(obj))
				onTrue.run(obj);
			else
				onFalse.run(obj);
		}

  }

	public class IfElsePredicate extends UnaryPredicate<ParamType> {
		private static final long serialVersionUID = 0;
		private final UnaryPredicate<ParamType> predicate;
		private final UnaryPredicate<ParamType> onTrue;
		private final UnaryPredicate<ParamType> onFalse;

		public IfElsePredicate(UnaryPredicate<ParamType> predicate, UnaryPredicate<ParamType> onTrue, UnaryPredicate<ParamType> onFalse) {
			this.predicate = predicate;
			this.onTrue = onTrue;
			this.onFalse = onFalse;
		}

		public boolean test(ParamType obj) {
			if (predicate.test(obj))
				return onTrue.test(obj);
			else
				return onFalse.test(obj);
		}

    public Functor optimize() {
			if (predicate.isAlwaysTrue())
				return onTrue;
			else if (predicate.isAlwaysFalse())
				return onFalse;
			return this;
		}
	}

	public class IfTrueFunction extends UnaryFunction<ParamType, Object> {
		private static final long serialVersionUID = 0;
		private final UnaryFunction<ParamType, ?> func;

		public IfTrueFunction(UnaryFunction<ParamType, ?> func) {
			this.func = func;
		}

		public Object evaluate(ParamType obj) {
			if (test(obj))
				return func.evaluate(obj);
			else
				return null;
		}

  }

	public class IfFalseFunction extends UnaryFunction<ParamType, Object> {
		private static final long serialVersionUID = 0;
		private final UnaryFunction<ParamType, ?> func;

		public IfFalseFunction(UnaryFunction<ParamType, ?> func) {
			this.func = func;
		}

		public Object evaluate(ParamType obj) {
			if (!test(obj))
				return func.evaluate(obj);
			else
				return null;
		}

  }

	private class IfElseFunction extends UnaryFunction<ParamType, Object> {
		private static final long serialVersionUID = 0;
		private final UnaryFunction<ParamType, ?> onTrue;
		private final UnaryFunction<ParamType, ?> onFalse;

		public IfElseFunction(UnaryFunction<ParamType, ?> onTrue, UnaryFunction<ParamType, ?> onFalse) {
			this.onTrue = onTrue;
			this.onFalse = onFalse;
		}

		public Object evaluate(ParamType obj) {
			if (test(obj))
				return onTrue.evaluate(obj);
			else
				return onFalse.evaluate(obj);
		}

  }

	public static class Binding<ParamType> extends Predicate {
		private static final long serialVersionUID = 0;
		private final UnaryPredicate<ParamType> predicate;
		private final ParamType value;

		public Binding(UnaryPredicate<ParamType> predicate, ParamType value) {
			this.predicate = predicate;
			this.value = value;
		}

		public boolean test() {
			return predicate.test(value);
		}

    public String toString() {
			return predicate + "(" + value + ")";
		}
	}

	public static class False<ParamType> extends UnaryPredicate<ParamType> {
		private static final long serialVersionUID = 0;

		public boolean isAlwaysFalse() {
			return true;
		}

		public boolean test(Object obj) {
			return false;
		}

		public UnaryPredicate not() {
			return TRUE;
		}

		/** Optimize out the extra predicate. */
		public UnaryPredicate and(UnaryPredicate predicate) {
			return this;
		}

		/** Optimize out the predicate. */
		public UnaryFunction ifTrue(UnaryFunction func) {
			return UnaryFunction.NULL;
		}

		/** Optimize out the predicate. */
		public UnaryFunction ifFalse(UnaryFunction func) {
			return func;
		}

		/** Optimize out the predicate. */
		public UnaryFunction ifElse(UnaryFunction onTrue, UnaryFunction onFalse) {
			return onFalse;
		}

		/** Optimize out the predicate. */
		public UnaryProcedure ifTrue(UnaryProcedure proc) {
			return UnaryProcedure.NoOp;
		}

		/** Optimize out the predicate. */
		public UnaryProcedure ifFalse(UnaryProcedure proc) {
			return proc;
		}

		/** Optimize out the predicate. */
		public UnaryProcedure ifElse(UnaryProcedure onTrue, UnaryProcedure onFalse) {
			return onFalse;
		}

		public String toString() {
			return "false";
		}

		public int hashCode() {
			return 1;
		}

		public boolean equals(Object object) {
			return object instanceof False;
		}
	}

	public static class True<ParamType> extends UnaryPredicate<ParamType> {
		private static final long serialVersionUID = 0;

		public boolean isAlwaysTrue() {
			return true;
		}

		public boolean test(Object obj) {
			return true;
		}

		public UnaryPredicate not() {
			return FALSE;
		}

		/** Optimize out the extra predicate. */
		public UnaryPredicate or(UnaryPredicate predicate) {
			return this;
		}

		/** Optimize out the predicate. */
		public UnaryFunction ifTrue(UnaryFunction func) {
			return func;
		}

		/** Optimize out the predicate. */
		public UnaryFunction ifFalse(UnaryFunction func) {
			return UnaryFunction.NULL;
		}

		/** Optimize out the predicate. */
		public UnaryFunction ifElse(UnaryFunction onTrue, UnaryFunction onFalse) {
			return onTrue;
		}

		/** Optimize out the predicate. */
		public UnaryProcedure ifTrue(UnaryProcedure proc) {
			return proc;
		}

		/** Optimize out the predicate. */
		public UnaryProcedure ifFalse(UnaryProcedure proc) {
			return UnaryProcedure.NoOp;
		}

		/** Optimize out the predicate. */
		public UnaryProcedure ifElse(UnaryProcedure onTrue, UnaryProcedure onFalse) {
			return onTrue;
		}

		public String toString() {
			return "true";
		}

		public int hashCode() {
			return 0;
		}

		public boolean equals(Object object) {
			return object instanceof True;
		}
	}
}
